use crate::core::services::job::job_context::JobContext;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::Arc;

pub struct StepContext {
    job_context: Arc<JobContext>,
}

async fn run_cmd<I, S>(
    program: impl ToString,
    args: I,
    work_dir: &PathBuf,
) -> std::io::Result<Output>
where
    I: IntoIterator<Item = S>,
    S: ToString,
{
    let program = program.to_string();
    let args = args.into_iter().map(|s| s.to_string()).collect::<Vec<_>>();
    let work_dir = work_dir.to_path_buf();

    tokio::spawn(async move {
        Command::new(program)
            .args(args)
            .current_dir(work_dir)
            .output()
    })
    .await?
}

impl StepContext {
    pub fn new(job_context: Arc<JobContext>) -> Self {
        Self { job_context }
    }

    pub fn job_context(&self) -> Arc<JobContext> {
        self.job_context.clone()
    }

    pub fn log<S: ToString>(&self, s: S) {
        self.job_context.send_log(s);
    }

    pub async fn run_cmd<I, S>(&self, program: &str, args: I) -> std::io::Result<Output>
    where
        I: IntoIterator<Item = S>,
        S: ToString,
    {
        let output = run_cmd(program, args, &self.job_context().path()).await?;

        self.log(std::str::from_utf8(&output.stderr).unwrap());
        self.log(std::str::from_utf8(&output.stdout).unwrap());

        Ok(output)
    }

    pub async fn run_cmd_at<I, S, P>(
        &self,
        program: &str,
        args: I,
        relative_path: P,
    ) -> std::io::Result<Output>
    where
        I: IntoIterator<Item = S>,
        S: ToString,
        P: AsRef<Path>,
    {
        let mut workspace = self.job_context.path();
        workspace.push(relative_path);
        let output = run_cmd(&program, args, &workspace.to_path_buf()).await?;

        self.log(std::str::from_utf8(&output.stderr).unwrap());
        self.log(std::str::from_utf8(&output.stdout).unwrap());

        Ok(output)
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::core::services::job::job_context::Workspace;
    use crate::core::test_util::TestUtil;
    use crate::core::util::Util;
    use tempdir::TempDir;

    #[tokio::test]
    async fn test_run_cmd() {
        let program = "echo";
        let param = "123";
        let output = run_cmd(program, vec![param], &PathBuf::from("."))
            .await
            .unwrap();

        assert_eq!(
            format!("{param}\n"),
            std::str::from_utf8(&output.stdout).unwrap()
        )
    }

    #[tokio::test]
    #[should_panic(expected = "No such file or directory")]
    async fn test_run_cmd_with_wrong_program() {
        let program = "wrong_program";
        let param = "";
        let output = run_cmd(program, vec![param], &PathBuf::from("."))
            .await
            .unwrap();
    }

    #[tokio::test]
    async fn test_run_cmd_at_home() {
        let program = "pwd";
        let param = "";
        let home = Util::user_dirs().home_dir().to_path_buf();
        let output = run_cmd(program, vec![param], &home).await.unwrap();
        assert_eq!(
            format!("{}\n", home.to_str().unwrap()),
            std::str::from_utf8(&output.stdout).unwrap()
        )
    }

    #[tokio::test]
    async fn test_step_context_run_cmd() {
        let step_ctx = TestUtil::get_step_context();

        let program = "echo";
        let param = "123";
        let output = step_ctx.run_cmd(program, vec![param]).await.unwrap();

        assert_eq!(
            format!("{param}\n"),
            std::str::from_utf8(&output.stdout).unwrap()
        )
    }

    #[tokio::test]
    #[should_panic(expected = "No such file or directory")]
    async fn test_step_context_run_cmd_with_wrong_program() {
        let step_ctx = TestUtil::get_step_context();

        let program = "wrong_program";
        let param = "";
        let output = step_ctx.run_cmd(program, vec![param]).await.unwrap();
    }

    #[tokio::test]
    async fn test_step_context_run_cmd_at_relative_path() {
        let step_ctx = TestUtil::get_step_context();

        let mut pwd = step_ctx.job_context.path();
        let relative_path = PathBuf::from("sub_dir");
        pwd.push(&relative_path);
        tokio::fs::create_dir(&pwd).await.unwrap();

        let program = "pwd";
        let param = "";
        let output = step_ctx
            .run_cmd_at(program, vec![param], &relative_path)
            .await
            .unwrap();

        assert_eq!(
            format!("{}\n", pwd.to_str().unwrap()),
            std::str::from_utf8(&output.stdout).unwrap()
        )
    }

    #[tokio::test]
    async fn test_log() {
        let step_ctx = Arc::new(TestUtil::get_step_context());
        let mut receiver = step_ctx.job_context.log_receiver();

        let handle = tokio::spawn(async move {
            step_ctx.log("Hello world");
        });

        assert_eq!(receiver.recv().await.unwrap(), "Hello world");
    }
}

